/*
 * Copyright 2009 Tran Nhut Trung.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */
package com.gdevelop.gwtxp.rebind;


import com.gdevelop.gwtxp.client.GwtXP;

import com.gdevelop.gwtxp.rebind.el.ELParserContext;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import java.io.File;
import java.io.PrintWriter;

import java.net.URL;

import java.util.Hashtable;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.jasper.compiler.JspUtil;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;


public class GwtXPCreator {
  private static final String IMPORT_JAVA_UTIL = "java.util.*";
  private static final String IMPORT_GWT_I18_CLIENT = "com.google.gwt.i18n.client.*";
  private static final String IMPORT_GWT_USER_CLIENT = "com.google.gwt.user.client.*";
  private static final String IMPORT_GWT_USER_CLIENT_UI = "com.google.gwt.user.client.ui.*";
  private static final String IMPORT_GWT_EVENT_DOM_CLIENT = "com.google.gwt.event.dom.client.*";
  private static final String IMPORT_GWT_EVENT_LOGICAL_SHARED = "com.google.gwt.event.logical.shared.*";
  private static final String IMPORT_GWTXP_CLIENT = "com.gdevelop.gwtxp.client.*";
  private static final String IMPORT_ECLIPSE_CONVERSION = "org.eclipse.core.databinding.conversion.gwt.*";
  private static final String IMPORT_ECLIPSE_DATABINDINGS = "org.eclipse.core.databinding.*";
  private static final String IMPORT_ECLIPSE_DATABINDINGS_OBSERVALE_LIST = "org.eclipse.core.databinding.observable.list.*";
  private static final String IMPORT_ECLIPSE_DATABINDINGS_OBSERVALE_VALUE = "org.eclipse.core.databinding.observable.value.*";
  private static final String IMPORT_ECLIPSE_DATABINDINGS_GWT = "org.eclipse.core.databinding.observable.gwt.*";
  private static final String IMPORT_ECLIPSE_DATABINDINGS_GWT2 = "org.eclipse.core.databinding.observable.gwt.internal.*";
  
  /**
   * Reserved names used during code generation
   */
  public static final String VAR_NAME_DATABINDING_CONTEXT = "__dbc__";
  public static final String VAR_NAME_ROOT_UI = "__guiObject__";
  public static final String VAR_NAME_UI_MAP = "__uiNameMap__";


  private GwtXPGenerator gwtXPGenerator;
  private TreeLogger logger = null;
  private GeneratorContext context = null;
  private File file;
  private Map<String, TagFactory> tagFactories;
  private SourceWriter out;

  private ELParserContext elContext;
  private Tag parent;
  private Tag root;
  
  private Hashtable tagVarNumbers = new Hashtable();
  
  private ClassSourceFileComposerFactory composer;
  
  private Map<String, Class> tagName2ClassMap = new Hashtable<String, Class>();

  public GwtXPCreator(GwtXPGenerator gwtXPGenerator, File file) {
    this.gwtXPGenerator = gwtXPGenerator;
    this.logger = gwtXPGenerator.getLogger();
    this.context = gwtXPGenerator.getGeneratorContext();
    this.tagFactories = gwtXPGenerator.tagFactories;
    this.file = file;
    
    logger = logger.branch(TreeLogger.INFO, "Generating code for file " + file);
  }
  
  public void generate() throws Exception {
    String packageName;
    String simpleName;
    
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    factory.setValidating(true);
    factory.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaLanguage", "http://www.w3.org/2001/XMLSchema");
    ClassLoader classLoader = this.getClass().getClassLoader();
    URL url = classLoader.getResource("META-INF/gwtXP.xsd");
    factory.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaSource", url.toString());
    
    // SchemaFactory schemaFactory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
    // @see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4996456
    // schemaFactory.setFeature("http://apache.org/xml/features/validation/schema-full-checking", false);
    // factory.setSchema(schemaFactory.newSchema(url));


    
    DocumentBuilder builder = factory.newDocumentBuilder();
    final SAXParseException[] parseExceptions = new SAXParseException[1];
    builder.setErrorHandler(new ErrorHandler(){
      public void warning(SAXParseException exception) {
        logger.log(TreeLogger.WARN, "Warning while parsing file " + file, exception);
      }
      public void error(SAXParseException exception) {
        logger.log(TreeLogger.ERROR, "Error while parsing file " + file + 
                                     " at " + exception.getLineNumber() + 
                                     ", " + exception.getColumnNumber(), exception);
        parseExceptions[0] = exception;
      }
      public void fatalError(SAXParseException exception) {
        logger.log(TreeLogger.ERROR, "Error while parsing file " + file + 
                                     " at " + exception.getLineNumber() + 
                                     ", " + exception.getColumnNumber(), exception);
        parseExceptions[0] = exception;
      }
    });
    Document doc = builder.parse(file);
    // Check parsing error
    if (parseExceptions[0] != null){
      throw new UnableToCompleteException();
    }
    
    Element docElement = doc.getDocumentElement();
    String controllerClassName = docElement.getAttribute("controllerClassName");
    if (controllerClassName == null){
      throw new RuntimeException("No controller in " + file.getPath());
    }
    elContext = new ELParserContext(gwtXPGenerator, controllerClassName);
    
    // Build the Tag hierachy
    Element rootElement = null;
    NodeList nl = docElement.getChildNodes();
    for (int i=0; i<nl.getLength(); i++){
      if (nl.item(i) instanceof Element){
        rootElement = (Element)nl.item(i);
        break;
      }
    }
    if (rootElement == null){
      throw new RuntimeException(file.getPath() + ": Tag " + docElement.getTagName() + " has no child element.");
    }
    visitElement(rootElement, 0);
    
    String[] packageAndClassName = Utils.getPackageAndClassName(file);
    packageName = packageAndClassName[0];
    simpleName = packageAndClassName[1] + GwtXPGenerator.GWTXP_SUFFIX;
    
    PrintWriter printWriter = context.tryCreate(this.logger, packageName, simpleName);
    if (printWriter == null){
      throw new RuntimeException("Could not create source code for " + file.getPath());
    }
    composer = new ClassSourceFileComposerFactory(packageName, simpleName);
    composer.setSuperclass(controllerClassName);
    composer.addImplementedInterface(GwtXP.class.getName());
    composer.addImport(IMPORT_JAVA_UTIL);
    composer.addImport(IMPORT_GWT_I18_CLIENT);
    composer.addImport(IMPORT_GWT_USER_CLIENT);
    composer.addImport(IMPORT_GWT_USER_CLIENT_UI);
    composer.addImport(IMPORT_GWT_EVENT_DOM_CLIENT);
    composer.addImport(IMPORT_GWT_EVENT_LOGICAL_SHARED);
    composer.addImport(IMPORT_GWTXP_CLIENT);
    composer.addImport(IMPORT_ECLIPSE_CONVERSION);
    composer.addImport(IMPORT_ECLIPSE_DATABINDINGS);
    composer.addImport(IMPORT_ECLIPSE_DATABINDINGS_OBSERVALE_LIST);
    composer.addImport(IMPORT_ECLIPSE_DATABINDINGS_OBSERVALE_VALUE);
    composer.addImport(IMPORT_ECLIPSE_DATABINDINGS_GWT);
    composer.addImport(IMPORT_ECLIPSE_DATABINDINGS_GWT2);
    addImports();

    out = composer.createSourceWriter(context, printWriter);
    // Setup SourceWriter for tags
    setSourceWriter();
    
    // generateBeanDeclarations(simpleName);
    generateUIDeclarations(simpleName);

    // implements GwtXP interface
    generateGwtXPMethods();

    // implements abstract methods
    generateControllerMethods();

    out.commit(logger);
  }

  private void visitElement(Element elem, int level) throws NotFoundException{
    String tagName = elem.getLocalName();
    String uri = elem.getNamespaceURI();
    TagFactory factory = tagFactories.get(uri);
    if (factory == null){
      throw new RuntimeException("No factory for " + elem.getBaseURI());
    }
    Tag tag = factory.createTag(tagName);
    if (tag == null){
      throw new RuntimeException("No tag for " + tagName);
    }

    tag.setELParserContext(elContext);
    if (parent != null){
      parent.add(tag);
    }else{
      root = tag;
    }
    
    String jName = elem.getAttribute("j_name");
    if ((jName != null) && (jName.length() > 0)){
      tag.setVarName(jName);
    }else{
      tag.setVarName(createTagVarName(elem.getNodeName(), elem.getPrefix(), elem.getLocalName()));
    }
    tag.setElement(elem);
    if (tag.getUIClass() != null){
      tagName2ClassMap.put(tag.getVarName(), tag.getUIClass());
    }

    // current Parent
    parent = tag;
    
    NodeList nl = elem.getChildNodes();
    for(int i=0, cnt=nl.getLength(); i<cnt; i++){
      if (nl.item(i) instanceof Element){
        visitElement((Element)nl.item(i), level+1);
      }
    }

    // current Parent
    parent = tag.getParent();
  }

  private String createTagVarName(String fullName, String prefix, String shortName){
    String varName;
    synchronized (tagVarNumbers) {
      varName = prefix + "_" + shortName + "_";
      if (tagVarNumbers.get(fullName) != null) {
        Integer i = (Integer)tagVarNumbers.get(fullName);
        varName = varName + i.intValue();
        tagVarNumbers.put(fullName, new Integer(i.intValue() + 1));
      } else {
        tagVarNumbers.put(fullName, new Integer(1));
        varName = varName + "0";
      }
    }
    return JspUtil.makeXmlJavaIdentifier(varName);
  }
  
  /**
   * Add additional imports required by tag generator
   */
  private void addImports(){
    addImports(root);
  }
  private void addImports(Tag me){
    for (String requiredImport : me.getImports()){
      composer.addImport(requiredImport);
    }
    
    for (Tag child : me.getChildren()){
      addImports(child);
    }
  }

  /**
   * Setup SourceWriter
   */
  private void setSourceWriter(){
    setSourceWriter(root);
  }
  private void setSourceWriter(Tag me){
    me.setSourceWriter(out);
    for (Tag child : me.getChildren()){
      setSourceWriter(child);
    }
  }
  
  /**
   * Generate widget declarations for tags
   */
  private void generateUIDeclarations(String className) throws NotFoundException {
    out.println("private " + className + " __this__ = this;");
    out.println("private DataBindingContext " + VAR_NAME_DATABINDING_CONTEXT + " = new GWTDataBindingContext();");
    out.println("private Map<String, UIObject> " + VAR_NAME_UI_MAP + " = new HashMap<String, UIObject>();");
    out.println("private UIObject " + VAR_NAME_ROOT_UI + ";");
    out.println();

    generateUIDeclarations(root);
  }
  private void generateUIDeclarations(Tag me) throws NotFoundException {
    me.generateDeclaration();
    for (Tag child : me.getChildren()){
      generateUIDeclarations(child);
    }
  }

  /**
   * Generate methods for GwtXP interface
   */
  private void generateGwtXPMethods() throws Exception{
    out.println();
    out.println("public DataBindingContext getDataBindingContext(){");
    out.indent();
    out.println("return " + VAR_NAME_DATABINDING_CONTEXT + ";");
    out.outdent();
    out.println("}");

    out.println();
    out.println("public UIObject getGUI(){");
    out.indent();
    out.println("if (" + VAR_NAME_ROOT_UI + " == null ){");
    out.indent();
    out.println(VAR_NAME_ROOT_UI + " = __initGUI__();");
    out.outdent();
    out.println("}");
    out.println("return " + VAR_NAME_ROOT_UI + ";");
    out.outdent();
    out.println("}");

    out.println();
    out.println("public UIObject getUI(String name){");
    out.indent();
    out.println("if (" + VAR_NAME_ROOT_UI + " == null ){");
    out.indent();
    out.println(VAR_NAME_ROOT_UI + " = __initGUI__();");
    out.outdent();
    out.println("}");
    out.println("return " + VAR_NAME_UI_MAP + ".get(name);");
    out.outdent();
    out.println("}");

    out.println();
    out.println("private UIObject __initGUI__(){");
    out.indent();
    generateInitUI(null, root);
    out.println();
    out.println("return " + VAR_NAME_ROOT_UI + ";");
    out.outdent();
    out.println("}");

  }
  
  /**
   * Generate methods which implements Controller's abtract methods
   * For a Controller's abtract method getXYZ, this method check whether 
   * there is a tag named XYZ and getXYZ's return typpe matches tag UIClass
   * Example: for abstract HasText getXYZ(), the tag named XYZ should have
   * UIClass which implements HasText
   */
  private void generateControllerMethods() throws NotFoundException {
    JClassType controllerType = elContext.getPageContextType();
    for (JMethod method : controllerType.getMethods()){
      String methodName = method.getName();
      JType ret = method.getReturnType();
      if (JPrimitiveType.VOID.equals(ret)){
        continue;
      }
      JClassType returnType = elContext.autoboxed(ret);
      String returnTypeName = returnType.getQualifiedSourceName();
      if (method.isAbstract() 
          && (method.getParameters().length == 0) 
          && (methodName.startsWith("get"))){
        // (TYPE_UIObject.isAssignableFrom(returnType))
        String j_name = methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
        // Check the return type
        Class uiClass = tagName2ClassMap.get(j_name);
        if (uiClass == null){
          continue;
        }
        JClassType tagUIType = context.getTypeOracle().getType(uiClass.getName());
        if (!returnType.isAssignableFrom(tagUIType)){
          continue;
        }

        out.println();
        out.println("public " + returnTypeName + " " + methodName + "(){");
        out.indent();
        out.println("getGUI();    // ensure GUI is initialized");
        // out.println("return (" + returnTypeName + ")" + VAR_NAME_UI_MAP + ".get(\"" + j_name + "\");");
        out.println("return (" + returnTypeName + ")" + j_name + ";");
        out.outdent();
        out.println("}");
      }
    }
    
    try{
      // For getRootPanel()
      //x JMethod method = controllerType.getMethod("getRootPanel", new JType[]{});
      JMethod method = ELParserContext.getMethod(controllerType, "getRootPanel", new JType[]{});
      if (method != null){
        out.println();
        out.println("public HasWidgets getRootPanel(){");
        out.indent();
        out.println();
        out.println("return RootPanel.get();");
        out.outdent();
        out.println("}");
      }
    }catch(NotFoundException e){
      // ignore
    }

    try{
      // For getRootPanel(String id)
      JType typeString = context.getTypeOracle().getType(String.class.getName());
      //x JMethod method = controllerType.getMethod("getRootPanel", new JType[]{typeString});
      JMethod method = ELParserContext.getMethod(controllerType, "getRootPanel", new JType[]{typeString});
      if (method != null){
        out.println();
        out.println("public HasWidgets getRootPanel(String id){");
        out.indent();
        out.println();
        out.println("return RootPanel.get(id);");
        out.outdent();
        out.println("}");
      }
    }catch(NotFoundException e){
      // ignore
    }
  }

  private void generateInitUI(Tag parent, Tag me) throws Exception {
    if (parent != null){
      parent.generateAddUI(me);
    }

    try{
      me.generateInitStart();
    }catch(Exception e){
      logger.log(TreeLogger.ERROR, "Error while translating tag " + 
        me.getElement().getLocalName() + " in file " + this.file
        // TODO: lineNumber, columnNumber
        // + " at (" + me.getNode().getStart().getLineNumber() + ", " + me.getNode().getStart().getColumnNumber() + ")"
      );
      throw e;
    }

    for (Tag child : me.getChildren()){
      generateInitUI(me, child);
    }
    
    try{
      me.generateInitEnd();
    }catch(Exception e){
      logger.log(TreeLogger.ERROR, "Error while translating tag " + 
        me.getElement().getLocalName() + " in file " + this.file
        // TODO: lineNumber, columnNumber
        // + " at (" + me.getNode().getStart().getLineNumber() + ", " + me.getNode().getStart().getColumnNumber() + ")"
      );
      throw e;
    }
  }
  
}
